/*
 * Copyright 2012 Marsrutai.lt
 * 
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 *   http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package lt.marsrutai.mobile.android.activity;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;

import lt.marsrutai.mobile.android.AppContext;
import lt.marsrutai.mobile.android.R;
import lt.marsrutai.mobile.android.activity.intent.FastestRoutesResultMapIntentHolder;
import lt.marsrutai.mobile.android.activity.intent.TaskIntentHolder;
import lt.marsrutai.mobile.android.adapter.MultiInfoWindowAdapter;
import lt.marsrutai.mobile.android.adapter.MultiInfoWindowAdapter.CustomInfoWindowAdapter;
import lt.marsrutai.mobile.android.manager.ActivityManager;
import lt.marsrutai.mobile.android.model.Route;
import lt.marsrutai.mobile.android.model.Segment;
import lt.marsrutai.mobile.android.model.SegmentPoint;
import lt.marsrutai.mobile.android.model.VehiclePosition;
import lt.marsrutai.mobile.android.service.TransportInfoResolver.TransportInfo;
import lt.marsrutai.mobile.android.task.GetVehiclePositionsTask;
import lt.marsrutai.mobile.android.task.GetVehiclePositionsTask.Criteria;
import lt.marsrutai.mobile.android.util.StringUtils;
import lt.marsrutai.mobile.android.util.UnitConversionUtils;
import lt.marsrutai.mobile.android.util.VehiclePositionManager;
import lt.marsrutai.mobile.android.util.VehiclePositionManager.GetVehiclePositionsTaskCriteriaProvider;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.os.Bundle;
import android.support.v4.content.LocalBroadcastManager;
import android.text.format.DateFormat;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.TextView;

import com.actionbarsherlock.app.ActionBar;
import com.google.android.gms.maps.GoogleMap;
import com.google.android.gms.maps.model.BitmapDescriptor;
import com.google.android.gms.maps.model.BitmapDescriptorFactory;
import com.google.android.gms.maps.model.LatLng;
import com.google.android.gms.maps.model.Marker;
import com.google.android.gms.maps.model.MarkerOptions;
import com.google.android.gms.maps.model.PolylineOptions;

/**
 * Displays a route on the map
 * 
 * @author Justas
 */
public class FastestRoutesResultMapActivity extends BaseMapActivity {
    
    private static final int TRANSPARENCY_MASK = 0xBBFFFFFF;
    
    private VehiclePositionManager vehiclePositionManager;
    private ActivityManager manager;
    
    private String scopeFolder;
    private Route route;
    private String title;
    
    private Map<String, RouteNode> markerNodeMap = new HashMap<String, RouteNode>();

    private BroadcastReceiver vehiclePositionReceiver = new BroadcastReceiver() {
        @Override
        public void onReceive(Context context, Intent intent) {
            TaskIntentHolder holder = new TaskIntentHolder(intent);
            List<VehiclePosition> vehiclePositions = holder.getListResult();

            if (holder.getStatus() == GetVehiclePositionsTask.RESULT_NOT_RESOLVED) {
                manager.makeToast(FastestRoutesResultMapActivity.this, getString(R.string.vehicle_position_could_not_acquire));
            }
            else {
                if (isMapReady()) {
                    vehiclePositionManager.drawVehiclePositions(getMap(), vehiclePositions);
                }
            }
        }
    };


    protected int getNavigationMode() {
        return ActionBar.NAVIGATION_MODE_STANDARD;
    }
    
    @Override
    public void onCreateInternal(Bundle savedInstanceState) {
        super.onCreateInternal(savedInstanceState);

        manager = new ActivityManager(this);

        receiveItemData(savedInstanceState);

        vehiclePositionManager = new VehiclePositionManager(this);
        vehiclePositionManager.setTaskCriteriaProvider(new GetVehiclePositionsTaskCriteriaProvider() {
            
            @Override
            public Criteria[] getCriteria() {
                List<GetVehiclePositionsTask.Criteria> criteriaList = new LinkedList<GetVehiclePositionsTask.Criteria>();
                for (Segment segment : route.getSegments()) {
                    if (segment.getTransportId() != null && segment.getScheduleName() != null) {
                        criteriaList.add(new GetVehiclePositionsTask.Criteria(scopeFolder, segment.getTransportId(), segment.getScheduleName()));
                    }
                }
                
                return criteriaList.toArray(new GetVehiclePositionsTask.Criteria[criteriaList.size()]);
            }
        });
        getTopRightControls().add(vehiclePositionManager.setUpControls((ViewGroup) findViewById(R.id.map_container)));
    }
    
    @Override
    protected void onPause() {
        super.onPause();
        
        LocalBroadcastManager.getInstance(this).unregisterReceiver(vehiclePositionReceiver);
        
        // cancel the timer
        vehiclePositionManager.cancel();
        vehiclePositionManager.clear();
    }
    
    @Override
    protected void onResumeInternal() {
        super.onResumeInternal();
        
        LocalBroadcastManager.getInstance(this).registerReceiver(vehiclePositionReceiver, new IntentFilter(GetVehiclePositionsTask.BROADCAST_ACTION));

        vehiclePositionManager.resume();
    }
    
    @Override
    protected void setUpMap(GoogleMap map, Bundle savedInstanceState) {
        super.setUpMap(map, savedInstanceState);
        
        displayStops();
        
        MultiInfoWindowAdapter adapter = new MultiInfoWindowAdapter();
        adapter.registerAdapter(new RouteInfoWindowAdapter());
        adapter.registerAdapter(vehiclePositionManager.getInfoWindowAdapter());
        map.setInfoWindowAdapter(adapter);
    }

    @Override
    protected void onSaveInstanceState(Bundle outState) {
        FastestRoutesResultMapIntentHolder intentHolder = new FastestRoutesResultMapIntentHolder(new Bundle());
        intentHolder.setRoute(route);
        intentHolder.setScopeFolder(scopeFolder);
        intentHolder.setTitle(title);
        
        outState.putAll(intentHolder.getIntent().getExtras());
        super.onSaveInstanceState(outState);
    }

    protected void receiveItemData(Bundle bundle) {
        FastestRoutesResultMapIntentHolder intentHolder;

        intentHolder = new FastestRoutesResultMapIntentHolder(bundle != null ? bundle : getIntent().getExtras());

        route = intentHolder.getRoute();
        scopeFolder = intentHolder.getScopeFolder();
        title = intentHolder.getTitle();
    }
    
    private void displayStops() {
        List<RouteNode> routeNodes = createRouteNodes();
        final List<LatLng> allGeoPoints = new LinkedList<LatLng>();
        PolylineOptions polylineOptions = new PolylineOptions();
        
        // set markers depending on route node type
        for (int i = 0; i < routeNodes.size(); i++) {
            RouteNode routeNode = routeNodes.get(i);
            LatLng point = new LatLng(routeNode.getPoint().getLatitude().doubleValue(), routeNode.getPoint().getLongitude().doubleValue());
            allGeoPoints.add(point);
            
            SegmentPoint segmentPoint = routeNode.getPoint();

            addToPolyline(polylineOptions, point);
            
            // check whether this is a stop and not a polyline point
            if (segmentPoint.getStopId() == null && segmentPoint.getTime() == null) {
                continue;
            }
        
            String title = segmentPoint.getStopId() != null ? segmentPoint.getStopName() : routeNode.getAlternativeName();
            
            MarkerOptions marker = new MarkerOptions()
                    .position(point)
                    .title(String.format("%s (%s)", title, DateFormat.getTimeFormat(this).format(segmentPoint.getTime().getTime())))
                    .snippet(segmentPoint.getStopId() != null ? segmentPoint.getStopDirectionName() : null);
            BitmapDescriptor icon = null;

            if (i == 0) {
                icon = BitmapDescriptorFactory.fromResource(R.drawable.first_stop);
            }
            else if (i == routeNodes.size() - 1) {
                icon = BitmapDescriptorFactory.fromResource(R.drawable.last_stop);
                
                polylineOptions = addPolylineToMap(polylineOptions, routeNode);
            }
            else if (routeNode.getType() == RouteNodeActionType.CHANGE || routeNode.getType() == RouteNodeActionType.GET_IN) {
                icon = BitmapDescriptorFactory.fromResource(R.drawable.segment_start);
                
                polylineOptions = addPolylineToMap(polylineOptions, routeNode);
                addToPolyline(polylineOptions, point);
            }
            else if (routeNode.getType() == RouteNodeActionType.GET_OUT) {
                icon = BitmapDescriptorFactory.fromResource(R.drawable.segment_end);
                
                polylineOptions = addPolylineToMap(polylineOptions, routeNode);
                addToPolyline(polylineOptions, point);
            }
            else {
                // In some cases, it is possible that there are multiple walk segments, these should be ignored and treated as polyline points
                if (segmentPoint.getStopId() == null) {
                    continue;
                }

                icon = BitmapDescriptorFactory.fromResource(R.drawable.all_stops);
            }
            
            marker.icon(icon);
            
            addMarker(marker, routeNode);
        }
        
        zoomToItems(allGeoPoints);
    }

    /**
     * Adds the provided polyline to the map, and prepares a new one.
     * 
     * @param polylineOptions
     * @param routeNode
     * @return
     */
    protected PolylineOptions addPolylineToMap(PolylineOptions polylineOptions, RouteNode routeNode) {
        // Add transparency to the color
        int transportColor = TRANSPARENCY_MASK & AppContext.getTransportInfoResolver().resolveTransportColor(scopeFolder, routeNode.getTransportId());
        getMap().addPolyline(polylineOptions.width(UnitConversionUtils.dpToPx(this, 8)).color(transportColor));

        return new PolylineOptions();
    }

    protected void addToPolyline(PolylineOptions polylineOptions, LatLng point) {
        polylineOptions.add(point);
    }

    /**
     * Adds a marker and maps it to the represented routeNode
     * 
     * @param markerOptions
     * @param routeNode
     */
    protected void addMarker(MarkerOptions markerOptions, RouteNode routeNode) {
        Marker marker = getMap().addMarker(markerOptions);
        markerNodeMap.put(marker.getId(), routeNode);
    }

    /**
     * Builds a list of nodes representing the current route
     * 
     * @return
     */
    protected List<RouteNode> createRouteNodes() {
        List<RouteNode> routeNodes = new ArrayList<RouteNode>();

        List<Segment> segments = route.getSegments();
        int indexOfLastSegment = segments.size() - 1;
        for (int segmentIndex = 0; segmentIndex <= indexOfLastSegment; segmentIndex++) {
            Segment segment = segments.get(segmentIndex);
            List<SegmentPoint> geoPoints = segment.getGeoPoints();
            int indexOfLastPoint = geoPoints.size() - 1;
            
            for (int pointIndex = 0; pointIndex <= indexOfLastPoint; pointIndex++) {
                SegmentPoint segmentPoint = geoPoints.get(pointIndex);
                
                /*
                 * point has no time - it represents a point on a polyline
                 * between stops (FIXME using absence of time to differentiate
                 * between polyline points and stops is not a good idea)
                 */
                if (segmentPoint.getTime() == null || segmentPoint.getStopId() == null) {
                    RouteNode routeNode = new RouteNode();
                    routeNode.setPoint(segmentPoint);
                    routeNode.setTransportId(segment.getTransportId());
                    routeNode.setType(RouteNodeActionType.RIDE);
                    routeNode.setScheduleName(segment.getScheduleName());
                    String alternativeName = null;
                    if (segmentIndex == 0) {
                        alternativeName = segment.getStartName();
                    }
                    else if(segmentIndex == indexOfLastSegment) {
                        alternativeName = segment.getEndName();
                    }
                    routeNode.setAlternativeName(alternativeName);
                    routeNodes.add(routeNode);
                }
                // Current stop is the same as previous - treat this node as a "change" node
                else if (routeNodes.size() > 0 && StringUtils.equals(routeNodes.get(routeNodes.size() - 1).point.getStopId(), segmentPoint.getStopId())) {
                    RouteNode routeNode = routeNodes.get(routeNodes.size() - 1);
                    
                    RouteNodeActionType type = RouteNodeActionType.CHANGE;
                    
                    if (pointIndex == 0 && previousSegmentExists(segmentIndex) && isWalkSegment(segments.get(segmentIndex - 1))) {
                        type = RouteNodeActionType.GET_IN;
                    }
                    else if (pointIndex == 0 && isWalkSegment(segments.get(segmentIndex))) {
                        type = RouteNodeActionType.GET_OUT;
                    }
                    
                    routeNode.setType(type);
                    routeNode.setTransportId2(segment.getTransportId());
                    routeNode.setScheduleName2(segment.getScheduleName());
                }
                else {
                    RouteNode routeNode = new RouteNode();
                    routeNode.setType(RouteNodeActionType.RIDE);
                    routeNode.setPoint(segmentPoint);
                    routeNode.setTransportId(segment.getTransportId());
                    routeNode.setScheduleName(segment.getScheduleName());
                    boolean firstStop = pointIndex == 0 && segmentIndex == 0;
                    boolean lastStop = pointIndex == indexOfLastPoint && segmentIndex == indexOfLastSegment;
                    routeNode.setAlternativeName(firstStop ? segment.getStartName() : (lastStop ? segment.getEndName() : null));
                    routeNodes.add(routeNode);
                }
            }
        }
        return routeNodes;
    }

    private boolean previousSegmentExists(int segmentIndex) {
        return segmentIndex > 0;
    }

    protected boolean isWalkSegment(Segment segment) {
        return segment.getTrackid() == null;
    }
    
    public static class RouteNode {
        
        private RouteNodeActionType type;
        private SegmentPoint point;
        private String transportId;
        private String scheduleName;

        private String transportId2;
        private String scheduleName2;
        
        private String alternativeName;

        public SegmentPoint getPoint() {
            return point;
        }

        public void setPoint(SegmentPoint point) {
            this.point = point;
        }

        public void setType(RouteNodeActionType type) {
            this.type = type;
        }

        public RouteNodeActionType getType() {
            return type;
        }

        public String getTransportId() {
            return transportId;
        }

        public void setTransportId(String transportId) {
            this.transportId = transportId;
        }

        public String getTransportId2() {
            return transportId2;
        }

        public void setTransportId2(String transportId2) {
            this.transportId2 = transportId2;
        }

        public String getScheduleName() {
            return scheduleName;
        }

        public void setScheduleName(String scheduleName) {
            this.scheduleName = scheduleName;
        }

        public String getScheduleName2() {
            return scheduleName2;
        }

        public void setScheduleName2(String scheduleName2) {
            this.scheduleName2 = scheduleName2;
        }

        public String getAlternativeName() {
            return alternativeName;
        }

        public void setAlternativeName(String alternativeName) {
            this.alternativeName = alternativeName;
        }

    }
    
    public enum RouteNodeActionType {
        RIDE, CHANGE, GET_OUT, GET_IN
    }
    
    private class RouteInfoWindowAdapter implements CustomInfoWindowAdapter {
        
        private final View contents;
        
        private TextView transportId1;
        private TextView transportId2;
        private ImageView transportIdSeparator;

        public RouteInfoWindowAdapter() {
            contents = getLayoutInflater().inflate(R.layout.route_node_info, null);
            transportId1 = (TextView) contents.findViewById(R.id.transportId1);
            transportId2 = (TextView) contents.findViewById(R.id.transportId2);
            transportIdSeparator = (ImageView) contents.findViewById(R.id.transportIdSeparator);
        }

        @Override
        public View getInfoWindow(Marker marker) {
            return null;
        }

        @Override
        public View getInfoContents(Marker marker) {
            render(marker, contents);
            return contents;
        }
        
        @Override
        public boolean isSupported(Marker marker) {
            return markerNodeMap.containsKey(marker.getId());
        }

        private void render(Marker marker, View view) {
            String title = marker.getTitle();
            TextView titleUi = ((TextView) view.findViewById(R.id.title));
            titleUi.setText(title);

            String snippet = marker.getSnippet();
            TextView snippetUi = ((TextView) view.findViewById(R.id.snippet));
            snippetUi.setText(snippet);

            RouteNode routeNode = markerNodeMap.get(marker.getId());
            
            transportId1.setVisibility(View.GONE);
            transportId2.setVisibility(View.GONE);
            transportIdSeparator.setVisibility(View.GONE);
            
            switch (routeNode.getType()) {
            case RIDE:
                fillViewInfo(transportId1, routeNode.getScheduleName(), routeNode.getTransportId(), scopeFolder);
                break;
            case CHANGE:
                fillViewInfo(transportId1, routeNode.getScheduleName(), routeNode.getTransportId(), scopeFolder);
                transportIdSeparator.setVisibility(View.VISIBLE);
                fillViewInfo(transportId2, routeNode.getScheduleName2(), routeNode.getTransportId2(), scopeFolder);
                break;
            case GET_IN:
                transportIdSeparator.setVisibility(View.VISIBLE);
                fillViewInfo(transportId2, routeNode.getScheduleName2(), routeNode.getTransportId2(), scopeFolder);
                break;
            case GET_OUT:
                transportIdSeparator.setVisibility(View.VISIBLE);
                fillViewInfo(transportId1, routeNode.getScheduleName(), routeNode.getTransportId(), scopeFolder);
                break;
            }
        }

        protected void fillViewInfo(TextView view, String scheduleName, String transportId, String scopeFolder) {
            if (scheduleName == null) {
                view.setVisibility(View.GONE);
                return;
            }
            
            view.setVisibility(View.VISIBLE);
            view.setText(scheduleName);
            
            TransportInfo info = AppContext.getTransportInfoResolver().getTransportInfo(scopeFolder, transportId);
            if (info != null) {
                view.setBackgroundColor(info.getColor());
            }
        }
    }

}
